package font;

import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Rectangle;
import java.awt.font.GlyphVector;
import java.awt.font.TextAttribute;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import opengl.renderer.RendererFactory;
import util.ResourceLoader;
import world.material.HieroSettings;
import color.GLColor;

public class GLFontUnicode extends GLFont {
	/** The number of display lists that will be cached for strings from this font */

	/** The AWT font that is being rendered */
	protected Font font;
	/** The reference to the True Type Font file that has kerning information */
	protected String ttfFileRef;

	/** The highest glyph code allowed */
	static private final int MAX_GLYPH_CODE = 0x10FFFF;
	/** The number of glyphs on a page */
	private static final int PAGE_SIZE = 512;
	/** The number of pages */
	private static final int PAGES = MAX_GLYPH_CODE / PAGE_SIZE;
	
	private static final Comparator<Glyph> heightComparator = new Comparator<Glyph>() {
		public int compare(Glyph o1, Glyph o2) {
			return o1.getHeight() - o2.getHeight();
		}
	};

	public int ascent;
	private int descent;
	private int leading;
	private int spaceWidth;

	private final Glyph[][] glyphs = new Glyph[PAGES][];
	private final List<GlyphPage> glyphPages = new ArrayList<GlyphPage>();
	public final List<Glyph> queuedGlyphs = new ArrayList<Glyph>(256);
	/** The effects that need to be applied to the font */
	private final List<Effect> effects = new ArrayList<Effect>();

	private int paddingTop;
	public int paddingLeft;
	private int paddingBottom;
	public int paddingRight;

	public int paddingAdvanceX;
	private int paddingAdvanceY;

	public Glyph missingGlyph;

	private int glyphPageWidth = 512;
	private int glyphPageHeight = 512;



	public GLFontUnicode(String ttfFileRef, String hieroFileRef) throws Exception {
		this(ttfFileRef, new HieroSettings(hieroFileRef));
	}

	public GLFontUnicode(String ttfFileRef, HieroSettings settings) throws Exception {
		this.ttfFileRef = ttfFileRef;
		Font font = createFont(ttfFileRef);
		initializeFont(font, settings.getFontSize(), settings.isBold(), settings.isItalic());
		loadSettings(settings);
	}

	public GLFontUnicode(String ttfFileRef, int size, boolean bold, boolean italic) throws Exception {
		this.ttfFileRef = ttfFileRef;
		initializeFont(createFont(ttfFileRef), size, bold, italic);
	}

	public GLFontUnicode(Font font, String hieroFileRef) throws Exception {
		this(font, new HieroSettings(hieroFileRef));
	}

	public GLFontUnicode(Font font, HieroSettings settings) {
		initializeFont(font, settings.getFontSize(), settings.isBold(), settings.isItalic());
		loadSettings(settings);
	}

	public GLFontUnicode(Font font) {
		initializeFont(font, font.getSize(), font.isBold(), font.isItalic());
	}

	public GLFontUnicode(Font font, int size, boolean bold, boolean italic) {
		initializeFont(font, size, bold, italic);
	}

	public void addGlyphs(int startCodePoint, int endCodePoint) {
		for (int codePoint = startCodePoint; codePoint <= endCodePoint; codePoint++) {
			addGlyphs(new String(Character.toChars(codePoint)));
		}
	}

	public void addGlyphs(String text) {
		if (text == null) {
			throw new IllegalArgumentException("text cannot be null.");
		}

		char[] chars = text.toCharArray();
		GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
		for (int i = 0, n = vector.getNumGlyphs(); i < n; i++) {
			int codePoint = text.codePointAt(vector.getGlyphCharIndex(i));
			Rectangle bounds = getGlyphBounds(vector, i, codePoint);
			getGlyph(vector.getGlyphCode(i), codePoint, bounds, vector, i);
		}
	}

	/**
	 * Queues the glyphs in the ASCII character set (codepoints 32 through 255) to be loaded.
	 * 
	 */
	public void addAsciiGlyphs() {
		addGlyphs(32, 255);
	}

	public void addNeheGlyphs() {
		addGlyphs(32, 32 + 96);
	}

	public boolean loadGlyphs() throws Exception {
		return loadGlyphs(-1);
	}

	public boolean loadGlyphs(int maxGlyphsToLoad) throws Exception {
		if (queuedGlyphs.isEmpty())
			return false;

		if (effects.isEmpty())
			throw new IllegalStateException("The FontUnicode must have at least one effect before any glyphs can be loaded.");

		for (Iterator<Glyph> iter = queuedGlyphs.iterator(); iter.hasNext();) {
			Glyph glyph = iter.next();
			int codePoint = glyph.getCodePoint();

			// Don't load an image for a glyph with nothing to display.
			if (glyph.getWidth() == 0 || codePoint == ' ') {
				iter.remove();
				continue;
			}

			// Only load the first missing glyph.
			if (glyph.isMissing()) {
				if (missingGlyph != null) {
					if (glyph != missingGlyph)
						iter.remove();
					continue;
				}
				missingGlyph = glyph;
			}
		}

		Collections.sort(queuedGlyphs, heightComparator);

		// Add to existing pages.
		for (Iterator<GlyphPage> iter = glyphPages.iterator(); iter.hasNext();) {
			GlyphPage glyphPage = iter.next();
			maxGlyphsToLoad -= glyphPage.loadGlyphs(queuedGlyphs, maxGlyphsToLoad);
			if (maxGlyphsToLoad == 0 || queuedGlyphs.isEmpty())
				return true;
		}

		// Add to new pages.
		while (!queuedGlyphs.isEmpty()) {
			GlyphPage glyphPage = new GlyphPage(this, glyphPageWidth, glyphPageHeight);
			glyphPages.add(glyphPage);
			maxGlyphsToLoad -= glyphPage.loadGlyphs(queuedGlyphs, maxGlyphsToLoad);
			if (maxGlyphsToLoad == 0)
				return true;
		}

		return true;
	}

	/**
	 * Clears all loaded and queued glyphs.
	 */
	public void clearGlyphs() {
		for (int i = 0; i < PAGES; i++)
			glyphs[i] = null;

		for (Iterator<GlyphPage> iter = glyphPages.iterator(); iter.hasNext();) {
			GlyphPage page = iter.next();
			try {
				page.getImage().destroy();
			} catch (Exception ignored) {
			}
		}
		glyphPages.clear();

		/*
		 * if (baseDisplayListID != -1) { GL.glDeleteLists(baseDisplayListID, displayLists.size()); baseDisplayListID = -1; }
		 */

		queuedGlyphs.clear();
		missingGlyph = null;
	}

	public void destroy() {
		clearGlyphs();
	}

	public int getWidth(String text) {
		return RendererFactory.getGLRendererFontUnicode().getWidth(this, text);
	}

	public int getHeight(String text) {
		return RendererFactory.getGLRendererFontUnicode().getHeight(this, text);
	}

	public int getLineHeight(String text) {
		return RendererFactory.getGLRendererFontUnicode().getHeight(this, text);
	}

	public void drawString(float x, float y, String text) {
		RendererFactory.getGLRendererFontUnicode().drawString(this, x, y, text);
	}

	public void drawString(float x, float y, String text, GLColor col) {
		RendererFactory.getGLRendererFontUnicode().drawString(this, x, y, text, col);
	}

	public void drawString(float x, float y, String text, GLColor col, int startIndex, int endIndex) {
		RendererFactory.getGLRendererFontUnicode().drawString(this, x, y, text, col, startIndex, endIndex);
	}

	public Glyph getGlyph(int glyphCode, int codePoint, Rectangle bounds, GlyphVector vector, int index) {
		if (glyphCode < 0 || glyphCode >= MAX_GLYPH_CODE) {
			return new Glyph(codePoint, bounds, vector, index, this) {
				public boolean isMissing() {
					return true;
				}
			};
		}
		int pageIndex = glyphCode / PAGE_SIZE;
		int glyphIndex = glyphCode & (PAGE_SIZE - 1);
		Glyph glyph = null;
		Glyph[] page = glyphs[pageIndex];
		if (page != null) {
			glyph = page[glyphIndex];
			if (glyph != null)
				return glyph;
		} else
			page = glyphs[pageIndex] = new Glyph[PAGE_SIZE];
		// Add glyph so size information is available and queue it so its image can be loaded later.
		glyph = page[glyphIndex] = new Glyph(codePoint, bounds, vector, index, this);
		queuedGlyphs.add(glyph);
		return glyph;
	}

	public Rectangle getGlyphBounds(GlyphVector vector, int index, int codePoint) {
		Rectangle bounds = vector.getGlyphPixelBounds(index, GlyphPage.renderContext, 0, 0);
		if (codePoint == ' ')
			bounds.width = spaceWidth;
		return bounds;
	}

	public int getSpaceWidth() {
		return spaceWidth;
	}

	public Font getFont() {
		return font;
	}

	public int getPaddingTop() {
		return paddingTop;
	}

	public void setPaddingTop(int paddingTop) {
		this.paddingTop = paddingTop;
	}

	public int getPaddingLeft() {
		return paddingLeft;
	}

	public void setPaddingLeft(int paddingLeft) {
		this.paddingLeft = paddingLeft;
	}

	public int getPaddingBottom() {
		return paddingBottom;
	}

	public void setPaddingBottom(int paddingBottom) {
		this.paddingBottom = paddingBottom;
	}

	public int getPaddingRight() {
		return paddingRight;
	}

	public void setPaddingRight(int paddingRight) {
		this.paddingRight = paddingRight;
	}

	public int getPaddingAdvanceX() {
		return paddingAdvanceX;
	}

	public void setPaddingAdvanceX(int paddingAdvanceX) {
		this.paddingAdvanceX = paddingAdvanceX;
	}

	public int getPaddingAdvanceY() {
		return paddingAdvanceY;
	}

	public void setPaddingAdvanceY(int paddingAdvanceY) {
		this.paddingAdvanceY = paddingAdvanceY;
	}

	public int getLineHeight() {
		return descent + ascent + leading + paddingTop + paddingBottom + paddingAdvanceY;
	}

	public int getAscent() {
		return ascent;
	}

	public int getDescent() {
		return descent;
	}

	public int getLeading() {
		return leading;
	}

	public int getGlyphPageWidth() {
		return glyphPageWidth;
	}

	public void setGlyphPageWidth(int glyphPageWidth) {
		this.glyphPageWidth = glyphPageWidth;
	}

	public int getGlyphPageHeight() {
		return glyphPageHeight;
	}

	public void setGlyphPageHeight(int glyphPageHeight) {
		this.glyphPageHeight = glyphPageHeight;
	}

	public List<GlyphPage> getGlyphPages() {
		return glyphPages;
	}

	public List<Effect> getEffects() {
		return effects;
	}

	/**
	 * Returns the path to the TTF file for this FontUnicode, or null. If this FontUnicode was created without specifying the TTF file, it will try to determine
	 * the path using Sun classes. If this fails, null is returned.
	 * 
	 * @return The reference to the font file that the kerning was loaded from
	 */
	public String getFontFile() {
		if (ttfFileRef == null) {
			// Worst case if this FontUnicode was loaded without a ttfFileRef, try to get the font file from Sun's classes.
			try {
				Object font2D = Class.forName("sun.font.FontManager").getDeclaredMethod("getFont2D", new Class[] { Font.class })
						.invoke(null, new Object[] { font });
				Field platNameField = Class.forName("sun.font.PhysicalFont").getDeclaredField("platName");
				platNameField.setAccessible(true);
				ttfFileRef = (String) platNameField.get(font2D);
			} catch (Throwable ignored) {
			}
			if (ttfFileRef == null)
				ttfFileRef = "";
		}
		if (ttfFileRef.length() == 0)
			return null;
		return ttfFileRef;
	}

	// ------------------------------------------------------------private
	private void loadSettings(HieroSettings settings) {
		paddingTop = settings.getPaddingTop();
		paddingLeft = settings.getPaddingLeft();
		paddingBottom = settings.getPaddingBottom();
		paddingRight = settings.getPaddingRight();
		paddingAdvanceX = settings.getPaddingAdvanceX();
		paddingAdvanceY = settings.getPaddingAdvanceY();
		glyphPageWidth = settings.getGlyphPageWidth();
		glyphPageHeight = settings.getGlyphPageHeight();
		effects.addAll(settings.getEffects());
	}

	private void initializeFont(Font baseFont, int size, boolean bold, boolean italic) {
		Map attributes = baseFont.getAttributes();
		attributes.put(TextAttribute.SIZE, new Float(size));
		attributes.put(TextAttribute.WEIGHT, bold ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR);
		attributes.put(TextAttribute.POSTURE, italic ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR);
		try {
			attributes.put(TextAttribute.class.getDeclaredField("KERNING").get(null), TextAttribute.class.getDeclaredField("KERNING_ON").get(null));
		} catch (Exception ignored) {
		}
		font = baseFont.deriveFont(attributes);

		FontMetrics metrics = GlyphPage.getScratchGraphics().getFontMetrics(font);
		ascent = metrics.getAscent();
		descent = metrics.getDescent();
		leading = metrics.getLeading();

		// Determine width of space glyph (getGlyphPixelBounds gives a width of zero).
		char[] chars = " ".toCharArray();
		GlyphVector vector = font.layoutGlyphVector(GlyphPage.renderContext, chars, 0, chars.length, Font.LAYOUT_LEFT_TO_RIGHT);
		spaceWidth = vector.getGlyphLogicalBounds(0).getBounds().width;
	}

	private static Font createFont(String ttfFileRef) throws Exception {
		try {
			return Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(ttfFileRef));
		} catch (FontFormatException ex) {
			throw new Exception("Invalid font: " + ttfFileRef, ex);
		} catch (IOException ex) {
			throw new Exception("Error reading font: " + ttfFileRef, ex);
		}
	}
}
